[小ネタ]shapelessでcase class間の変換を行う
shapelessによるcase classの自動変換を試してみました。
はじめに
scalaでコードを書いていると似ているけど微妙に違いcase classの間でデータの詰め替えを行うケースがよくあります(e.g. インフラレイヤーのDBレコードからドメインオブジェクトへ)。shapeless を使うとそのようなデータの詰め替えを簡単に実装できます。
build.sbt
今回のbuild.sbtは以下の通りです。
scalaVersion := "2.13.2" libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % "2.1.1", "com.chuusai" %% "shapeless" % "2.3.3" )
概要
ざっくりと書き出すと以下のような方法でcaseクラス間の変換をします。
- 変換元クラスのLabelledGenericによって元のインスタンスからフィールド名ラベル付きのHListを生成する
- Align, Intersectionによってフィールドの並び替えと共通フィールドの絞り込みを行う
- 変換後クラスのLabelledGenericによってHListからインスタンスを生成する
コード
以上を下記のように変換前後の型を表すFrom、Toを型パラメータとしてとるtrait MapToとして定義しコンパニオンオブジェクトでその実装を参照できるようにします。apply
とinstance
は冗長に見えますがinstanceのimplicit パラメータのマクロによる解決のためにこのような実装が必要になります。
package example import cats.Eq import cats.implicits._ import shapeless._ import shapeless.ops.hlist /** * Fromのインスタンスから各フィールド値をコピーしてToのインスタンスを生成する。From、Toは以下の条件をみたす必要がある * - Toのフィールドがすべて、同じ型、同じ名前でFromにフィールドとして存在する * @tparam From コピー元の型 * @tparam To コピー先の型 */ sealed trait MapTo[From, To] { def apply(a: From): To } object MapTo { def apply[From, To](implicit mapTo: MapTo[From, To]): MapTo[From, To] = mapTo implicit def instance[From, To, FromRepr <: HList, ToRepr <: HList, Unaligned <: HList]( implicit from: LabelledGeneric.Aux[From, FromRepr], to: LabelledGeneric.Aux[To, ToRepr], inter: hlist.Intersection.Aux[FromRepr, ToRepr, Unaligned], align: hlist.Align[Unaligned, ToRepr] ): MapTo[From, To] = new MapTo[From, To] { override def apply(a: From): To = to.from(inter(from.to(a))) } } object MapExample extends App { final case class PersonWithPhoneNumber(name: String, age: Int, address: Address, phoneNumber: String) final case class Address(pref: String, city: String, street: String) final case class Person(name: String, age: Int, address: Address) // 順序が違う final case class Person2(age: Int, address: Address, name:String) // PersonWithPhoneNumberにないフィールド final case class Person3(name:String, age: Int, address: Address, color:String) implicit val personWithPhoneNumberEq: Eq[PersonWithPhoneNumber] = Eq.fromUniversalEquals implicit val personEq: Eq[Person] = Eq.fromUniversalEquals implicit val person2Eq: Eq[Person2] = Eq.fromUniversalEquals implicit val addressEq: Eq[Address] = Eq.fromUniversalEquals //生成元 val from = PersonWithPhoneNumber("ann", 2, Address("Hokkaido", "Sapporo", "Bird Street"), "090-1234-5678") assert( MapTo[PersonWithPhoneNumber, Person].apply(from) === Person("ann", 2, Address("Hokkaido", "Sapporo", "Bird Street")) ) assert( MapTo[PersonWithPhoneNumber, Person2].apply(from) === Person2(2, Address("Hokkaido", "Sapporo", "Bird Street"), "ann") ) // コンパイルエラー // Error:(66, 8) could not find implicit value for parameter mapTo: example.MapTo[example.MapExample.PersonWithPhoneNumber,example.MapExample.Person3] // MapTo[PersonWithPhoneNumber, Person3] }
HListに対する他の操作
今回紹介したcase class間の変換を含むHListの操作の例がAdvanced Scala with Shapless Courseで紹介されていますのでそちらも合わせてどうぞ。